package com.github.mikephil.charting.charts;

import android.content.Context;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;

import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
import com.github.mikephil.charting.data.DataSet;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.utils.Highlight;
import com.github.mikephil.charting.utils.Utils;
import com.github.mikephil.charting.utils.XLabels.XLabelPosition;

import java.util.ArrayList;

/**
 * Chart that draws bars.
 * 
 * @author Philipp Jahoda
 */
public class BarChart extends BarLineChartBase<BarData> {

	/** indicates the angle of the 3d effect */
	private float mSkew = 0.3f;

	/** indicates how much the 3d effect goes back */
	private float mDepth = 0.3f;

	/** flag the enables or disables 3d bars */
	private boolean m3DEnabled = false;

	/** flag that enables or disables the highlighting arrow */
	private boolean mDrawHighlightArrow = false;

	/**
	 * if set to true, all values are drawn above their bars, instead of below
	 * their top
	 */
	private boolean mDrawValueAboveBar = true;

	/**
	 * if set to true, all values of a stack are drawn individually, and not
	 * just their sum
	 */
	private boolean mDrawValuesForWholeStack = true;

	/**
	 * if set to true, a grey area is darawn behind each bar that indicates the
	 * maximum value
	 */
	private boolean mDrawBarShadow = true;

	/** the rect object that is used for drawing the bar shadow */
	private RectF mBarShadow = new RectF();

	/** the rect object that is used for drawing the bars */
	private RectF mBarRect = new RectF();

	public BarChart(Context context) {
		super(context);
	}

	public BarChart(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	public BarChart(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}

	@Override
	protected void init() {
		super.init();

		mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
		mHighlightPaint.setStyle(Paint.Style.FILL);
		mHighlightPaint.setColor(Color.rgb(0, 0, 0));
		// set alpha after color
		mHighlightPaint.setAlpha(120);

		// calculate3DColors();
	}

	@Override
	protected void calcMinMax(boolean fixedValues) {
		super.calcMinMax(fixedValues);

		// increase deltax by 1 because the bars have a width of 1
		mDeltaX++;

		// extend xDelta to make space for multiple datasets (if ther are one)
		mDeltaX *= mOriginalData.getDataSetCount();

		int maxEntry = 0;

		for (int i = 0; i < mOriginalData.getDataSetCount(); i++) {

			DataSet<? extends Entry> set = mOriginalData.getDataSetByIndex(i);

			if (maxEntry < set.getEntryCount())
				maxEntry = set.getEntryCount();
		}

		float groupSpace = mOriginalData.getGroupSpace();
		mDeltaX += maxEntry * groupSpace;
	}

	@Override
	protected void drawHighlights() {

		int setCount = mOriginalData.getDataSetCount();

		for (int i = 0; i < mIndicesToHightlight.length; i++) {

			Highlight h = mIndicesToHightlight[i];
			int index = h.getXIndex();

			int dataSetIndex = h.getDataSetIndex();
			BarDataSet set = (BarDataSet) mCurrentData.getDataSetByIndex(dataSetIndex);

			if (set == null)
				continue;

			mHighlightPaint.setColor(set.getHighLightColor());
			mHighlightPaint.setAlpha(set.getHighLightAlpha());

			// check outofbounds
			if (index < mCurrentData.getYValCount() && index >= 0 && index < (mDeltaX * mPhaseX) / mOriginalData.getDataSetCount()) {

				Entry e = getEntryByDataSetIndex(index, dataSetIndex);

				if (e == null)
					continue;

				// calculate the correct x-position
				float x = index * setCount + dataSetIndex + mOriginalData.getGroupSpace() / 2f + mOriginalData.getGroupSpace() * index;
				float y = e.getVal();

				prepareBar(x, y, set.getBarSpace());

				mDrawCanvas.drawRect(mBarRect, mHighlightPaint);

				if (mDrawHighlightArrow) {

					mHighlightPaint.setAlpha(255);

					// distance between highlight arrow and bar
					float offsetY = mDeltaY * 0.07f;

					Path arrow = new Path();
					arrow.moveTo(x + 0.5f, y + offsetY * 0.3f);
					arrow.lineTo(x + 0.2f, y + offsetY);
					arrow.lineTo(x + 0.8f, y + offsetY);

					transformPath(arrow);
					mDrawCanvas.drawPath(arrow, mHighlightPaint);
				}
			}
		}
	}

	@Override
	protected void drawData() {

		ArrayList<BarDataSet> dataSets = mOriginalData.getDataSets();
		int setCount = mOriginalData.getDataSetCount();

		// the space between bar-groups
		float space = mOriginalData.getGroupSpace();

		// 2D drawing
		for (int i = 0; i < setCount; i++) {

			BarDataSet dataSet = dataSets.get(i);
			boolean noStacks = dataSet.getStackSize() == 1 ? true : false;

			ArrayList<BarEntry> entries = dataSet.getYVals();

			// do the drawing
			for (int j = 0; j < dataSet.getEntryCount() * mPhaseX; j++) {

				BarEntry e = entries.get(j);

				// calculate the x-position, depending on datasetcount
				float x = e.getXIndex() + j * (setCount - 1) + i + space * j + space / 2f;
				float y = e.getVal();

				// no stacks
				if (noStacks) {

					prepareBar(x, y, dataSet.getBarSpace());

					// avoid drawing outofbounds values
					if (isOffContentRight(mBarRect.left))
						break;

					if (isOffContentLeft(mBarRect.right)) {
						continue;
					}

					// if drawing the bar shadow is enabled
					if (mDrawBarShadow) {
						mRenderPaint.setColor(dataSet.getBarShadowColor());
						mDrawCanvas.drawRect(mBarShadow, mRenderPaint);
					}

					// Set the color for the currently drawn value. If the index
					// is
					// out of bounds, reuse colors.
					mRenderPaint.setColor(dataSet.getColor(j));
					mDrawCanvas.drawRect(mBarRect, mRenderPaint);

				} else { // stacked bars

					float[] vals = e.getVals();

					// we still draw stacked bars, but there could be one
					// non-stacked
					// in between
					if (vals == null) {

						prepareBar(x, y, dataSet.getBarSpace());

						// if drawing the bar shadow is enabled
						if (mDrawBarShadow) {
							mRenderPaint.setColor(dataSet.getBarShadowColor());
							mDrawCanvas.drawRect(mBarShadow, mRenderPaint);
						}

						mRenderPaint.setColor(dataSet.getColor(0));
						mDrawCanvas.drawRect(mBarRect, mRenderPaint);

					} else {

						float all = e.getVal();

						// if drawing the bar shadow is enabled
						if (mDrawBarShadow) {

							prepareBar(x, y, dataSet.getBarSpace());
							mRenderPaint.setColor(dataSet.getBarShadowColor());
							mDrawCanvas.drawRect(mBarShadow, mRenderPaint);
						}

						// draw the stack
						for (int k = 0; k < vals.length; k++) {

							all -= vals[k];

							prepareBar(x, vals[k] + all, dataSet.getBarSpace());

							mRenderPaint.setColor(dataSet.getColor(k));
							mDrawCanvas.drawRect(mBarRect, mRenderPaint);
						}
					}

					// avoid drawing outofbounds values
					if (isOffContentRight(mBarRect.left))
						break;
				}
			}
		}
	}

	/**
	 * Prepares a bar for drawing on the specified x-index and y-position. Also
	 * prepares the shadow-bar if enabled.
	 * 
	 * @param x
	 *            the x-position
	 * @param y
	 *            the y-position
	 * @param barspace
	 *            the space between bars
	 */
	private void prepareBar(float x, float y, float barspace) {

		float spaceHalf = barspace / 2f;
		float left = x + spaceHalf;
		float right = x + 1f - spaceHalf;
		float top = y >= 0 ? y : 0;
		float bottom = y <= 0 ? y : 0;

		mBarRect.set(left, top, right, bottom);

		transformRectWithPhase(mBarRect);

		// if a shadow is drawn, prepare it too
		if (mDrawBarShadow) {
			mBarShadow.set(mBarRect.left, mOffsetTop, mBarRect.right, getHeight() - mOffsetBottom);
		}
	}
	
	@Override
	protected void calcModulus() {
		float[] values = new float[9];
		mMatrixTouch.getValues(values);

		//2014/11/12 新增功能:垂直顯示文字
		if (mXLabels.getPosition() == XLabelPosition.TOP_INSIDE_VERTICAL) {
			mXLabels.mXAxisLabelModulus = (int) Math.ceil((mCurrentData.getXValCount() * mXLabels.mLabelHeight) / (mContentRect.width() * values[Matrix.MSCALE_X]));
		} else {
			mXLabels.mXAxisLabelModulus = (int) Math.ceil((mCurrentData.getXValCount() * mXLabels.mLabelWidth) / (mContentRect.width() * values[Matrix.MSCALE_X]));
		}

	}

	@Override
	protected void drawXLabels(float yPos) {

		// pre allocate to save performance (dont allocate in loop)
		float[] position = new float[] { 0f, 0f };

		int step = mCurrentData.getDataSetCount();

		for (int i = 0; i < mCurrentData.getXValCount(); i += mXLabels.mXAxisLabelModulus) {

			position[0] = i * step + i * mOriginalData.getGroupSpace() + mOriginalData.getGroupSpace() / 2f;

			// center the text
			if (mXLabels.isCenterXLabelsEnabled())
				position[0] += (step / 2f);
			
			/*
			 //嘗試修正: 沒有對齊的問題
			float[] values = new float[9];
			mMatrixTouch.getValues(values);
			if (values[Matrix.MSCALE_X] == 1) {
				position[0] += 0.2f;
			}
			*/

			transformPointArray(position);

			if (position[0] >= mOffsetLeft && position[0] <= getWidth() - mOffsetRight) {

				String label = mCurrentData.getXVals().get(i);

				if (mXLabels.isAvoidFirstLastClippingEnabled()) {

					// avoid clipping of the last
					if (i == mCurrentData.getXValCount() - 1) {
						float width = Utils.calcTextWidth(mXLabelPaint, label);

						if (width > getOffsetRight() * 2 && position[0] + width > getWidth())
							position[0] -= width / 2;

						// avoid clipping of the first
					} else if (i == 0) {

						float width = Utils.calcTextWidth(mXLabelPaint, label);
						position[0] += width / 2;
					}
				}
				// 2014/11/12 新增功能: 垂直顯示文字
				if (mXLabels.getPosition() == XLabelPosition.TOP_INSIDE_VERTICAL) {
					// 重新計算座標
					float fTopAlign = yPos;
					String label1 = mCurrentData.getXVals().get(i);
					float width = Utils.calcTextWidth(mXLabelPaint, label1);
					fTopAlign = yPos + width / 2 ;
					// 繪圖
					mDrawCanvas.save(); // 儲存目前狀態,以防rotate變更(PUSH)
					mDrawCanvas.rotate(-90, position[0], fTopAlign);// 旋轉90度  注x,y 是原始的位置。
					mDrawCanvas.drawText(label, position[0], fTopAlign, mXLabelPaint);
					mDrawCanvas.restore();// 還原先前狀態,(POP)
				} else {
					mDrawCanvas.drawText(label, position[0], yPos, mXLabelPaint);
				}
			}
		}
	}

	@Override
	protected void drawVerticalGrid() {

		if (!mDrawVerticalGrid || mCurrentData == null)
			return;

		float[] position = new float[] { 0f, 0f };

		// take into consideration that multiple DataSets increase mDeltaX
		int step = mCurrentData.getDataSetCount();

		for (int i = 0; i < mCurrentData.getXValCount(); i += mXLabels.mXAxisLabelModulus) {

			position[0] = i * step + i * mOriginalData.getGroupSpace();

			transformPointArray(position);

			if (position[0] >= mOffsetLeft && position[0] <= getWidth()) {

				mDrawCanvas.drawLine(position[0], mOffsetTop, position[0], getHeight() - mOffsetBottom, mGridPaint);
			}
		}
	}

	@Override
	protected void drawValues() {

		// if values are drawn
		if (mDrawYValues && mCurrentData.getYValCount() < mMaxVisibleCount * mScaleX) {

			ArrayList<BarDataSet> dataSets = ((BarData) mCurrentData).getDataSets();

			float posOffset = 0f;
			float negOffset = 0f;

			// calculate the correct offset depending on the draw position of
			// the value
			if (mDrawValueAboveBar) {
				posOffset = -Utils.convertDpToPixel(5);
				negOffset = Utils.calcTextHeight(mValuePaint, "8") * 1.5f;
			} else {
				posOffset = Utils.calcTextHeight(mValuePaint, "8") * 1.5f;
				negOffset = -Utils.convertDpToPixel(5);
			}

			for (int i = 0; i < mCurrentData.getDataSetCount(); i++) {

				BarDataSet dataSet = dataSets.get(i);
				ArrayList<BarEntry> entries = dataSet.getYVals();

				float[] valuePoints = generateTransformedValuesBarChart(entries, i);

				// if only single values are drawn (sum)
				if (!mDrawValuesForWholeStack) {

					for (int j = 0; j < valuePoints.length * mPhaseX; j += 2) {

						if (isOffContentRight(valuePoints[j]))
							break;

						if (isOffContentLeft(valuePoints[j]) || isOffContentTop(valuePoints[j + 1]) || isOffContentBottom(valuePoints[j + 1]))
							continue;

						float val = entries.get(j / 2).getVal();

						drawValue(val, valuePoints[j], valuePoints[j + 1] + (val >= 0 ? posOffset : negOffset));
					}

					// if each value of a potential stack should be drawn
				} else {

					for (int j = 0; j < (valuePoints.length - 1) * mPhaseX; j += 2) {

						if (isOffContentRight(valuePoints[j]))
							break;

						if (isOffContentLeft(valuePoints[j]) || isOffContentTop(valuePoints[j + 1]) || isOffContentBottom(valuePoints[j + 1]))
							continue;

						BarEntry e = entries.get(j / 2);

						float[] vals = e.getVals();

						// we still draw stacked bars, but there is one
						// non-stacked
						// in between
						if (vals == null) {

							drawValue(e.getVal(), valuePoints[j], valuePoints[j + 1] + (e.getVal() >= 0 ? posOffset : negOffset));

						} else {

							float[] transformed = new float[vals.length * 2];
							int cnt = 0;
							float add = e.getVal();

							for (int k = 0; k < transformed.length; k += 2) {

								add -= vals[cnt];
								transformed[k + 1] = (vals[cnt] + add) * mPhaseY;
								cnt++;
							}

							transformPointArray(transformed);

							for (int k = 0; k < transformed.length; k += 2) {

								drawValue(vals[k / 2], valuePoints[j], transformed[k + 1] + (vals[k / 2] >= 0 ? posOffset : negOffset));
							}
						}
					}
				}
			}
		}
	}

	/**
	 * Draws a value at the specified x and y position.
	 * 
	 * @param value
	 * @param xPos
	 * @param yPos
	 */
	private void drawValue(float val, float xPos, float yPos) {

		String value = mValueFormatter.getFormattedValue(val);

		if (mDrawUnitInChart) {

			mDrawCanvas.drawText(value + mUnit, xPos, yPos, mValuePaint);
		} else {

			mDrawCanvas.drawText(value, xPos, yPos, mValuePaint);
		}
	}

	/**
	 * Returns the Highlight object (contains x-index and DataSet index) of the
	 * selected value at the given touch point inside the BarChart.
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	@Override
	public Highlight getHighlightByTouchPoint(float x, float y) {

		if (mDataNotSet || mCurrentData == null) {
			Log.e(LOG_TAG, "Can't select by touch. No data set.");
			return null;
		}

		// create an array of the touch-point
		float[] pts = new float[2];
		pts[0] = x;
		pts[1] = y;

		Matrix tmp = new Matrix();

		// invert all matrixes to convert back to the original value
		mMatrixOffset.invert(tmp);
		tmp.mapPoints(pts);

		mMatrixTouch.invert(tmp);
		tmp.mapPoints(pts);

		mMatrixValueToPx.invert(tmp);
		tmp.mapPoints(pts);

		// for barchart, we only need x-val
		double xTouchVal = pts[0];
		double base = xTouchVal;

		if (xTouchVal < 0 || xTouchVal > mDeltaX)
			return null;

		if (base < 0)
			base = 0;
		if (base >= mDeltaX)
			base = mDeltaX - 1;

		int setCount = mOriginalData.getDataSetCount();
		int valCount = setCount * mOriginalData.getXValCount();

		// calculate the amount of bar-space between index 0 and touch position
		float space = (float) (((float) valCount / (float) setCount) / (mDeltaX / base));

		float reduction = (float) space * mOriginalData.getGroupSpace();

		int xIndex = (int) ((base - reduction) / setCount);

		int dataSetIndex = ((int) (base - reduction)) % setCount;

		if (dataSetIndex == -1)
			return null;

		return new Highlight(xIndex, dataSetIndex);
	}

	/**
	 * Returns the bounding box of the specified Entry in the specified DataSet.
	 * Returns null if the Entry could not be found in the charts data.
	 * 
	 * @param e
	 * @param dataSetIndex
	 * @return
	 */
	public RectF getBarBounds(BarEntry e) {

		BarDataSet set = mOriginalData.getDataSetForEntry(e);

		if (set == null)
			return null;

		float barspace = set.getBarSpace();
		float y = e.getVal();
		float x = e.getXIndex();

		float spaceHalf = barspace / 2f;
		float left = x + spaceHalf;
		float right = x + 1f - spaceHalf;
		float top = y >= 0 ? y : 0;
		float bottom = y <= 0 ? y : 0;

		RectF bounds = new RectF(left, top, right, bottom);

		transformRect(bounds);

		return bounds;
	}

	/**
	 * sets the skew (default 0.3f), the skew indicates how much the 3D effect
	 * of the chart is turned to the right
	 * 
	 * @param skew
	 */
	public void setSkew(float skew) {
		this.mSkew = skew;
	}

	/**
	 * returns the skew value that indicates how much the 3D effect is turned to
	 * the right
	 * 
	 * @return
	 */
	public float getSkew() {
		return mSkew;
	}

	/**
	 * set the depth of the chart (default 0.3f), the depth indicates how much
	 * the 3D effect of the chart goes back
	 * 
	 * @param depth
	 */
	public void setDepth(float depth) {
		this.mDepth = depth;
	}

	/**
	 * returhs the depth, which indicates how much the 3D effect goes back
	 * 
	 * @return
	 */
	public float getDepth() {
		return mDepth;
	}

	/**
	 * if enabled, chart will be drawn in 3d
	 * 
	 * @param enabled
	 */
	public void set3DEnabled(boolean enabled) {
		this.m3DEnabled = enabled;
	}

	/**
	 * returns true if 3d bars is enabled, false if not
	 * 
	 * @return
	 */
	public boolean is3DEnabled() {
		return m3DEnabled;
	}

	/**
	 * set this to true to draw the highlightning arrow
	 * 
	 * @param enabled
	 */
	public void setDrawHighlightArrow(boolean enabled) {
		mDrawHighlightArrow = enabled;
	}

	/**
	 * returns true if drawing the highlighting arrow is enabled, false if not
	 * 
	 * @return
	 */
	public boolean isDrawHighlightArrowEnabled() {
		return mDrawHighlightArrow;
	}

	/**
	 * If set to true, all values are drawn above their bars, instead of below
	 * their top.
	 * 
	 * @param enabled
	 */
	public void setDrawValueAboveBar(boolean enabled) {
		mDrawValueAboveBar = enabled;
	}

	/**
	 * returns true if drawing values above bars is enabled, false if not
	 * 
	 * @return
	 */
	public boolean isDrawValueAboveBarEnabled() {
		return mDrawValueAboveBar;
	}

	/**
	 * if set to true, all values of a stack are drawn individually, and not
	 * just their sum
	 * 
	 * @param enabled
	 */
	public void setDrawValuesForWholeStack(boolean enabled) {
		mDrawValuesForWholeStack = enabled;
	}

	/**
	 * returns true if all values of a stack are drawn, and not just their sum
	 * 
	 * @return
	 */
	public boolean isDrawValuesForWholeStackEnabled() {
		return mDrawValuesForWholeStack;
	}

	/**
	 * If set to true, a grey area is drawn behind each bar that indicates the
	 * maximum value. Enabling his will reduce performance by about 50%.
	 * 
	 * @param enabled
	 */
	public void setDrawBarShadow(boolean enabled) {
		mDrawBarShadow = enabled;
	}

	/**
	 * returns true if drawing shadows (maxvalue) for each bar is enabled, false
	 * if not
	 * 
	 * @return
	 */
	public boolean isDrawBarShadowEnabled() {
		return mDrawBarShadow;
	}

	@Override
	protected void drawAdditional() {
	}
}
